<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 */
class DMModuleManager {

	/**
	 * @var array Array of DMBridgeModule objects
	 */
	private static $all_modules;

	/**
	 * @var DMModuleManager Singleton instance
	 */
	private static $instance;

	/**
	 * @var DMConfigXML
	 */
	private $config_xml;

	/**
	 * @var string
	 */
	private $modules_pathname;

	/**
	 * Destroys the Singleton instance.
	 *
	 * @return void
	 */
	public static function destroyInstance() {
		self::$instance = null;
	}

	/**
	 * @return DMModuleManager Shared instance of a DMModuleManager
	 */
	public static function getInstance() {
		if (!self::$instance) {
			self::$instance = new DMModuleManager();
		}
		return self::$instance;
	}

	protected function __construct() {
		$this->config_xml = DMConfigXML::getInstance();
		$this->setModulesPathname(
				dirname(__FILE__) . "/../../extensions/modules");
	}

	public function __clone() {
		//trigger_error("Cannot clone a Singleton");
	}

	public function __wakeup() {
		//trigger_error("Cannot deserialize a Singleton");
	}

	/**
	 * Alias of getAllModules(), called during dmBridge initialization. No
	 * need to call manually.
	 */
	public function bootstrapAllModules() {
		$this->getAllModules();
	}

	/**
	 * Returns an array of all compatible modules detected by the system.
	 *
	 * @return array Array of DMBridgeModule objects
	 * @since 2.0
	 */
	public function getAllModules() {
		if (self::$all_modules == null) {
			self::$all_modules = array();

			$it = new RecursiveDirectoryIterator($this->getModulesPathname());
			foreach (new RecursiveIteratorIterator($it) as $filename) {
				if (substr(basename($filename), -4, 4) == ".php"
						&& basename($filename, ".php") == basename(dirname($filename))) {

					$class = basename($filename, ".php");
					include_once(dirname($filename) . "/"
							. DMAbstractDMBridgeModule::BOOTSTRAP_FILENAME);
					self::$all_modules[] = new $class;
				}
			}
		}
		return self::$all_modules;
	}

	/**
	 * Used for dependency injection.
	 */
	public function setConfigXML(DMConfigXML $config_xml) {
		$this->config_xml = $config_xml;
		self::$all_modules = null;
	}

	/**
	 * @return array Array of DMBridgeModule objects
	 * @since 2.0
	 */
	public function getEnabledModules() {
		$modules = array();
		foreach ($this->getAllModules() as $module) {
			if ($this->isModuleEnabled($module)) {
				$modules[] = $module;
			}
		}
		return $modules;
	}

	/**
	 * @param string name
	 * @return DMBridgeModule 
	 */
	public function getModuleByName($name) {
		foreach ($this->getAllModules() as $module) {
			if ($module->getName() == $name) {
				return $module;
			}
		}
		return null;
	}

	/**
	 * @param mixed module A DMBridgeModule object, or the name of a module.
	 * @return boolean
	 */
	public function isModuleEnabled($module) {
		return $this->config_xml->isModuleEnabled($module);
	}

	/**
	 * @param DMBridgeModule module
	 * @param bool enabled
	 * @return void
	 * @throws DMModuleActivationException
	 */
	public function setModuleEnabled(DMBridgeModule $module, $enabled) {
		// is the module compatible with the current system version?
		if ($module->getMinSupportedVersionSequence()
				> DMBridgeVersion::getDmBridgeVersionSequence()) {
			throw new DMModuleActivationException(
					DMLocalizedString::getString("INCOMPATIBLE_MODULE"));
		}

		// do we need to prepare the data store?
		if ($enabled && $module instanceof DMBridgeDataStoreModule) {
			$ds = DMDataStoreFactory::getDataStore();
			$stmts = array();
			// Re-run the setup just to make sure the tables are set up. This
			// will probably fail but that's OK.
			switch ($ds->getType()) {
			case DMDataStoreType::PDO_MySQL:
				$stmts = $module->getSetupSQLForMySQL();
				break;
			case DMDataStoreType::PDO_SQLite:
				$stmts = $module->getSetupSQLForSQLite();
				break;
			}

			$ds->beginTransaction();
			foreach ($stmts as $sql) {
				try {
					$ds->write($sql, array());
				} catch (DMPDOException $e) {
					// if a create command failed for some reason OTHER than
					// the tables already existing, roll back and throw an
					// exception.
					if (strpos($e->getMessage(), "already exists") === false) {
						$ds->rollBack();
						throw new DMModuleActivationException($e->getMessage());
					}
				}
			}
			$ds->commit();

			if (!$this->config_xml->isModuleOfSameNameAndVersionInstalled($module)) {
				$stmts = array();
				if ($this->config_xml->isModuleOfSameNameInstalled($module)) { // upgrade
					switch ($ds->getType()) {
					case DMDataStoreType::PDO_MySQL:
						$stmts = $module->getUpgradeSQLForMySQL();
						break;
					case DMDataStoreType::PDO_SQLite:
						$stmts = $module->getUpgradeSQLForSQLite();
						break;
					}
				}
				$ds->beginTransaction();
				foreach ($stmts as $sql) {
					try {
						$ds->write($sql, array());
					} catch (DMPDOException $e) {
						$msg = DMLocalizedString::getString(
								sprintf("MODULE_ACTIVATION_SQL_FAILED", $sql));
						$ds->rollBack();
						throw new DMModuleActivationException($msg);
					}
				}
				$ds->commit();
			}
		}
		return $this->config_xml->setModuleEnabled($module, $enabled);
	}

	/**
	 * @return string
	 */
	public function getModulesPathname() {
		return $this->modules_pathname;
	}
	
	/**
	 * @param string pathname 
	 */
	public function setModulesPathname($pathname) {
		$this->modules_pathname = $pathname;
	}

}
